﻿using System.Collections.Generic;
using System.Globalization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RaptorEngine.Components;
using RaptorEngine.Components.Drawable;
using RaptorEngine.Debugs;
using RaptorEngine.Particles;
using RaptorEngine.Threads;
using RaptorEngine.Components.CollidableComponent;

namespace RaptorEngine.Managers
{
    /// <summary>
    ///  This class handles the drawing of scenes. It can be 
    ///  used to access the game's GraphicsDevice-class and
    ///  to set lighting settings.
    /// </summary>
    public class RenderManager
    {

        private readonly GraphicsDevice _graphics;
        private SpriteBatch _spriteBatch;
        public GraphicsDevice GraphicsDevice { get { return _graphics; } }
        //Multithreading
		private Dictionary<int, Drawable> _drawables = new Dictionary<int, Drawable>();
        private Dictionary<int, Camera> _cameras = new Dictionary<int, Camera>();
		private Stack<Drawable> _newDrawables = new Stack<Drawable>();
        private Stack<Camera> _newCameras = new Stack<Camera>();
	    public Dictionary<string, ParticleSystem> ParticleSystems { get; private set; }

	    public RenderManager(GraphicsDevice graphics)
        {
            _graphics = graphics;
            BoundingVolumeRenderer.Initialize(_graphics, 33);
        }

		private void Draw2D(List<Drawable2DComponent> drawables)
        {
            _spriteBatch.Begin();
			foreach (Drawable2DComponent drawable in drawables)
            {
				drawable.Draw(_spriteBatch);
            }
            _spriteBatch.End();
        }

        private void Draw3D(List<Drawable3DComponent> drawables, Scene scene)
        {
            Camera camera = null;

            if (GameEngine.Instance.Debug.UseDebugCam)
            {
                camera = GameEngine.Instance.Debug.DebugCamera;
            }
            else if (scene.HasCamera)
            {
                camera = scene.Camera;
            }

            if (camera != null)
            {
                _graphics.BlendState = BlendState.Opaque;
                _graphics.DepthStencilState = DepthStencilState.Default;

                foreach (Drawable3DComponent drawable in drawables)
                {
                    drawable.Draw(Matrix.Identity, camera.view, camera.projection);
                }
            }
        }

        private void DrawParticles(GameTime gameTime, Scene scene)
        {
            Camera camera = null;

            if (GameEngine.Instance.Debug.UseDebugCam)
            {
                camera = GameEngine.Instance.Debug.DebugCamera;
            }
            else if (scene.HasCamera)
            {
                camera = scene.Camera;
            }

            foreach (ParticleSystem system in ParticleSystems.Values)
            {
                if (camera != null)
                {
                    system.SetCamera(camera.view, camera.projection);
                    system.Draw(gameTime);
                }
            }
        }

        private void DrawBoundingVolumes(Scene scene)
        {

            Camera camera = null;

            if (GameEngine.Instance.Debug.UseDebugCam)
            {
                camera = GameEngine.Instance.Debug.DebugCamera;
            }
            else if (scene.HasCamera)
            {
                camera = scene.Camera;
            }

            if (camera != null)
            {
                foreach (Collidable c in scene.GetCollidableComponents())
                {
                    foreach (var bv in c.BoundingVolumes)
                    {
                        if (bv.Value.Sphere != null)
                            BoundingVolumeRenderer.Draw(bv.Value.Sphere.Value, camera.view, camera.projection);
                        else if (bv.Value.Box != null)
                            BoundingVolumeRenderer.Draw(bv.Value.Box.Value, bv.Value.Radius, bv.Value.Center, camera.view, camera.projection);
                    }   
                }
            }
        }

        internal void LoadContent()
        {
	        foreach (ParticleSystem system in ParticleSystems.Values)
	        {
		        system.LoadContent();
	        }
            _spriteBatch = new SpriteBatch(_graphics);
        }

        internal void Draw(GameTime gameTime)
		{
            //Counts the number of objects culled by frustum culling
            int cullCount = 0;

			foreach (ParticleSystem system in ParticleSystems.Values)
	        {
		        system.Update(gameTime);
	        }

		    ChangeBuffer renderBuffer;
		    GameEngine.Instance.ChangeBuffers.StartRenderProcessing(out renderBuffer, out gameTime);
		    ApplyChanges(renderBuffer, gameTime);

            //lists used to draw 2D and 3D components separately
			List<Drawable2DComponent> components2D = new List<Drawable2DComponent>();
			List<Drawable3DComponent> components3D = new List<Drawable3DComponent>();

            //stack used to draw scenes in a correct order
            Stack<Scene> renderStack = new Stack<Scene>();

            lock (GameEngine.Instance.Scene.Scenes)
            {
                foreach (Scene scene in GameEngine.Instance.Scene.Scenes)
                {
                    renderStack.Push(scene);
                }
            }

            _graphics.Clear(Color.Black);

            foreach (Scene scene in renderStack)
            {
                if (scene.IsVisible)
                {
                    components2D.Clear();
                    components3D.Clear();
                    foreach (Entity entity in scene.Entities)
                    {
                        
                        if (entity.HasComponent(ComponentType.Drawable2D))
                            components2D.Add((Drawable2DComponent) entity.GetComponent(ComponentType.Drawable2D));

                        if (entity.HasComponent(ComponentType.Drawable3D))
                        {
                            Drawable3DComponent temp = (Drawable3DComponent)entity.GetComponent(ComponentType.Drawable3D);
                            //Frustum Culling
                            if (EntityInView(entity, scene.Camera))
                            {
                                components3D.Add(temp);
                            }
                            else
                            {
                                cullCount++;
                            }
                            //components3D.Add((Drawable3DComponent) entity.GetComponent(ComponentType.Drawable3D));
                        }
                    }

                    Draw3D(components3D, scene);
                    if(GameEngine.Instance.Debug.DrawBoundingVolumes)
                        DrawBoundingVolumes(scene);
                    Draw2D(components2D);
                    DrawParticles(gameTime, scene);
                }
            }


            GameEngine.Instance.Debug.SetData("Culled Objects", cullCount.ToString(CultureInfo.InvariantCulture));
            //draw debug data
            if (GameEngine.Instance.Debug.ShowDebugData)
            {
                _spriteBatch.Begin();
                GameEngine.Instance.Debug.Draw(gameTime, _spriteBatch);
                _spriteBatch.End();
            }
			GameEngine.Instance.ChangeBuffers.SubmitRender();
        }

        private bool EntityInView(Entity entity, Camera camera)
        {
            Collidable collider;
            if(entity.HasComponent(ComponentType.Collidable))
            {

                collider = (Collidable)entity.GetComponent(ComponentType.Collidable);
                if (collider.BoundingVolumes["default"].Sphere != null)
                {
                    BoundingSphere sphere = (BoundingSphere)collider.BoundingVolumes["default"].Sphere;
                    return camera.InView(sphere);
                }

                if (collider.BoundingVolumes["default"].Box != null)
                {
                    BoundingBox box = (BoundingBox)collider.BoundingVolumes["default"].Box;
                    return camera.InView(box);
                }
            }
            return false;
            
        }

        /// <summary>
        ///  A method used to apply changes from the change buffer
        /// </summary>
	    private void ApplyChanges(ChangeBuffer renderBuffer, GameTime gt)
	    {
		    foreach (ChangeMessage message in renderBuffer.Messages)
		    {
			    switch (message.MessageType)
			    {
					case ChangeMessageType.UpdateWorldMatrix:
						_drawables[message.ID].worldMatrix = message.WorldMatrix;
						break;
					case ChangeMessageType.UpdateTextString:
						_drawables[message.ID].textString = message.TextString;
						break;
					case ChangeMessageType.UpdateAnimationState:
						_drawables[message.ID].animationState = message.AnimationState;
						break;
					case ChangeMessageType.UpdateCameraView:
					    _cameras[message.ID].view = message.CameraViewMatrix;
						break;
					case ChangeMessageType.UpdateCameraProjection:
                        _cameras[message.ID].projection = message.CameraProjection;
						break;
					case ChangeMessageType.CreateNewRenderData:
						UpdateRenderData(gt);
						break;
					case ChangeMessageType.DeleteRenderData:
                        if (_drawables.ContainsKey(message.ID))
					        _drawables.Remove(message.ID);
                        else if (_cameras.ContainsKey(message.ID))
                            _cameras.Remove(message.ID);
					    break;
			    }
		    }
			renderBuffer.Clear();
	    }

        /// <summary>
        ///  A method that is called when new drawables
        ///  are added, for multithreading purposes.
        /// </summary>
	    internal void AddNewData(Drawable drawable)
	    {
		    lock (_newDrawables)
		    {
				_newDrawables.Push(drawable);
		    }
            GameEngine.Instance.UpdateBuffer.Add(new ChangeMessage
                {
                MessageType = ChangeMessageType.CreateNewRenderData
            });
	    }

        /// <summary>
        ///  A method that is called when new cameras
        ///  are added, for multithreading purposes.
        /// </summary>
        internal void AddNewData(Camera camera)
        {
            lock (_newCameras)
            {
                _newCameras.Push(camera);
            }
            GameEngine.Instance.UpdateBuffer.Add(new ChangeMessage
                {
                MessageType = ChangeMessageType.CreateNewRenderData
            });
        }

        /// <summary>
        ///  A method that is called if new drawables or 
        ///  cameras are added, for multithreading purposes.
        /// </summary>
		private void UpdateRenderData(GameTime gt)
		{
			lock (_newDrawables)
			{
				while (_newDrawables.Count > 0)
				{
					Drawable temp = _newDrawables.Pop();
					if (_drawables.ContainsKey(temp.Id))
					{
						_drawables.Remove(temp.Id);
					}
					_drawables.Add(temp.Id, temp);
				}
			}

            lock (_newCameras)
            {
                while (_newCameras.Count > 0)
                {
                    Camera temp = _newCameras.Pop();
                    if (_cameras.ContainsKey(temp.Id))
                    {
                        _cameras.Remove(temp.Id);
                    }
                    _cameras.Add(temp.Id, temp);
                }
            }
		}

		internal void Initialize(Dictionary<string, ParticleSystem> particleSystems)
		{
			ParticleSystems = particleSystems;
			foreach (ParticleSystem system in particleSystems.Values)
			{
				system.Initialize();
			}
		}
	}
}
